﻿namespace Microsoft.Sdc.Tasks
{
    #region Using directives

    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Text;

    using Microsoft.Build.Framework;

    #endregion

    public class TrimJavascript : TaskBase
    {
        private const string CommentStart = "/*";
        private const string CommentEnd = "*/";
        private const string CommentLine = "//";
        private const string TraceLine = "SdcJsTrace";

        private bool commenting = false;
        
        private string rootFolder;

        /// <summary>
        /// Specifies the root folder from which to search for JS files.
        /// </summary>
        /// <value>path to</value>
        [Required]
        public string RootFolder
        {
            get { return this.rootFolder; }

            set { this.rootFolder = value; }
        }

        /// <summary>
        /// The task
        /// </summary>
        protected override void InternalExecute()
        {
            this.Log.LogMessage("TrimJavascript.InternalExecute [root:= '{0}']", this.RootFolder);

            if (!Directory.Exists(this.RootFolder))
            {
                throw new TaskException("TrimJavascript.FolderNotFound", this.RootFolder);
            }

            DirectoryInfo root = new DirectoryInfo(this.RootFolder);
            this.FindAndTrimAllJsFiles(root);
        }

        /// <summary>
        /// Finds all the files in the given folder looking for js files to trim.
        /// Recurses any folders.
        /// </summary>
        /// <param name="folder">to search for js files</param>
        private void FindAndTrimAllJsFiles(DirectoryInfo folder)
        {
            foreach (FileSystemInfo fsi in folder.GetFileSystemInfos())
            {
                if (fsi is FileInfo)
                {
                    FileInfo fi = (FileInfo)fsi;
                    if (string.Compare(fsi.Extension, ".js", true) == 0)
                    {
                        this.TrimFile(fi);
                    }
                }
                else if (fsi is DirectoryInfo)
                {
                    // recurse the folder
                    this.FindAndTrimAllJsFiles(fsi as DirectoryInfo);
                }
            }
        }

        /// <summary>
        /// Trim all extraneous info from the given js file
        /// </summary>
        /// <param name="fileToTrim">file to process</param>
        private void TrimFile(FileInfo fileToTrim)
        {
            this.Log.LogMessage("TrimJavascript.TrimFile({0})...", fileToTrim.FullName);

            FileInfo backupFile = new FileInfo(fileToTrim.FullName + ".bak");
            if (backupFile.Exists) { backupFile.Attributes = backupFile.Attributes & ~FileAttributes.ReadOnly; }
            fileToTrim.CopyTo(backupFile.FullName, true);

            FileAttributes attrbutesToRestore = fileToTrim.Attributes;  // nb as we'll have to put it back afterwards
            fileToTrim.Attributes = attrbutesToRestore & ~(FileAttributes.ReadOnly|FileAttributes.Hidden); // ensure the file is writeable

            using (TextWriter tw = fileToTrim.CreateText())
            {
                // The SdcTrace file is a special case: We just want to truncate it.
                // (which we will have done by creating replacing with textfile above).
                if (string.Compare(fileToTrim.Name, "SdcJsTrace.js", true) != 0)
                {
                    using (TextReader tr = backupFile.OpenText())
                    {
                        string lineRead = string.Empty;

                        while ((lineRead = tr.ReadLine()) != null)
                        {
                            string lineToWrite = TrimWhitespace(lineRead);

                            if (lineToWrite.Length != 0)
                            {
                                // look for an eol comment, e.g. " code; // blah"
                                lineToWrite = RemoveEolComment(lineToWrite);

                                if (lineToWrite.Length != 0)
                                {
                                    // look for a comment block, e.g. "code; /* blah */"
                                    lineToWrite = RemoveCommentBlock(lineToWrite);

                                    if (lineToWrite.Length != 0)
                                    {
                                        lineToWrite = RemoveTracing(lineToWrite);
                                    }
                                }
                            }

                            if (lineToWrite.Length != 0) { tw.WriteLine(lineToWrite); }
                        }
                    }
                }
            }

            fileToTrim.Attributes = attrbutesToRestore;
            this.Log.LogMessage("TrimJavascript.TrimFile ...done");
        }

        private string TrimWhitespace(string lineToWrite)
        {
            return lineToWrite.Trim(new char[] { ' ', '\t' });
        }

        private string RemoveEolComment(string lineToWrite)
        {
            return this.RemoveTokenToEol(lineToWrite, TrimJavascript.CommentLine);
        }

        private string RemoveCommentBlock(string lineToWrite)
        {
            this.commenting = this.StripToken(ref lineToWrite, TrimJavascript.CommentStart, TrimJavascript.CommentEnd, this.commenting);
            return lineToWrite;
        }

        private string RemoveTracing(string lineToWrite)
        {
            return this.RemoveTokenToEol(lineToWrite, TrimJavascript.TraceLine);
        }

        private string RemoveTokenToEol(string lineToWrite, string token)
        {
            int index;
            if ((index = lineToWrite.IndexOf(token)) != -1)
            {
                lineToWrite = TrimWhitespace(lineToWrite.Remove(index));
            }
            return lineToWrite;
        }

        /// <summary>
        /// Removes a delimited string from a given string, e.g. a comment block "/* blah */"
        /// </summary>
        /// <param name="lineToWrite">the string to parse, by ref, so also the parsed string</param>
        /// <param name="tokenStart">the starting token to search, e.g. "/*"</param>
        /// <param name="tokenEnd">the end token, e.g. "*/"</param>
        /// <param name="tokenFound">initial state (for multi-line tokens)</param>
        /// <returns>whether the token is currently found (when spanning multiple line)</returns>
        private bool StripToken(ref string lineToWrite, string tokenStart, string tokenEnd, bool tokenFound)
        {
            int startIndex = 0, endIndex = 0;

            if (!tokenFound)
            {
                startIndex = lineToWrite.IndexOf(tokenStart);
                if (startIndex != -1) { tokenFound = true; }
                else { startIndex = 0; }
            }

            if (tokenFound)
            {
                endIndex = lineToWrite.IndexOf(tokenEnd);
                if (endIndex != -1)
                {
                    tokenFound = false; // because we just "unfound" it
                    endIndex = endIndex + tokenEnd.Length; // want the end of the token not start of token.
                }
                else { endIndex = lineToWrite.Length; }
            }

            lineToWrite = TrimWhitespace(lineToWrite.Remove(startIndex, endIndex - startIndex));

            // if the endIndex != 0 then comment closure was found - we need to recurse to catch 
            // multiple tag scenario, e.g.:
            //     foo5;/*inline*/bar4;foo6;/*inline*/bar5;
            if ((!tokenFound) && (lineToWrite.Length != 0) && (endIndex != 0)) {
                tokenFound = this.StripToken(ref lineToWrite, tokenStart, tokenEnd, tokenFound);
            }

            return tokenFound;
        }
    }
}
